using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.IO;
using System.ComponentModel;
using System.Configuration;

using Icodeon.Util;
using Icodeon.Scorm2CC.Exceptions;

// Logging
//#if LOG4NET
using log4net;
using log4net.Config;
using log4net.Core;
//#endif

namespace Icodeon.Scorm2CC
{
    public class Scorm2ccConverter
    {
        #region Private Members

        private FileInfo[] inputFiles;
        private DirectoryInfo output;
        private FileInfo xslFileForScorm1p2;
        private FileInfo xslFileForScorm2004;
        private int overallProgress; //0-100
        private string overallStatus;

        /// <summary>
        /// The Status of the Scorm2cc "StartConversion" Object.
        /// </summary>
        private static Scorm2ccStatus conversionStatus = Scorm2ccStatus.Stopped;

        /// <summary>
        /// The "Cancel" Setting. Set to true to Cancel the Scorm2cc "StartConversion" method.
        /// </summary>
        private static bool cancelConversion = false;

        #endregion

        #region Constructor

        /// <summary>
        /// The Default Constructor for the Converter Object
        /// </summary>
        public Scorm2ccConverter()
        { }

        #endregion

        #region Event Related Methods

        /// <summary>
        /// The 'ConversionStart' Event
        /// </summary>
        public event ConversionStartEventHandler ConversionStart;

        /// <summary>
        /// The 'On Conversion Start' Event Handler: Called when.....
        /// </summary>
        /// <param name="e">The 'Conversion Start' Event Arguments.</param>
        protected virtual void OnConversionStart(ConversionStartEventArgs e)
        {
            ConversionStartEventHandler Handler = ConversionStart;
            if (Handler != null)
            {
                foreach (ConversionStartEventHandler Caster in Handler.GetInvocationList())
                {
                    ISynchronizeInvoke SyncInvoke = Caster.Target as ISynchronizeInvoke;
                    try
                    {
                        if (SyncInvoke != null && SyncInvoke.InvokeRequired)
                            SyncInvoke.Invoke(Handler, new object[] { this, e });
                        else
                            Caster(this, e);
                    }
                    catch
                    { }
                }
            }
        }


        /// <summary>
        /// The 'ConversionProgress' Event
        /// </summary>
        public event ConversionProgressEventHandler ConversionProgress;

        /// <summary>
        /// The 'On Conversion Progress' Event Handler: Called when to update the progress bar
        /// </summary>
        /// <param name="e">The 'Conversion Progress' Event Arguments.</param>
        protected virtual void OnConversionProgress(ConversionProgressEventArgs e)
        {
            ConversionProgressEventHandler Handler = ConversionProgress;
            if (Handler != null)
            {
                foreach (ConversionProgressEventHandler Caster in Handler.GetInvocationList())
                {
                    ISynchronizeInvoke SyncInvoke = Caster.Target as ISynchronizeInvoke;
                    try
                    {
                        if (SyncInvoke != null && SyncInvoke.InvokeRequired)
                            SyncInvoke.Invoke(Handler, new object[] { this, e });
                        else
                            Caster(this, e);
                    }
                    catch
                    { }
                }
            }
        }

        /// <summary>
        /// The 'ConversionComplete' Event
        /// </summary>
        public event ConversionCompleteEventHandler ConversionComplete;

        /// <summary>
        /// The 'On Conversion Complete' Event Handler: Called when.....
        /// </summary>
        /// <param name="e">The 'Conversion Complete' Event Arguments.</param>
        protected virtual void OnConversionComplete(ConversionCompleteEventArgs e)
        {
            ConversionCompleteEventHandler Handler = ConversionComplete;
            if (Handler != null)
            {
                foreach (ConversionCompleteEventHandler Caster in Handler.GetInvocationList())
                {
                    ISynchronizeInvoke SyncInvoke = Caster.Target as ISynchronizeInvoke;
                    try
                    {
                        if (SyncInvoke != null && SyncInvoke.InvokeRequired)
                            SyncInvoke.Invoke(Handler, new object[] { this, e });
                        else
                            Caster(this, e);
                    }
                    catch
                    { }
                }
            }
        }

        /// <summary>
        /// The 'Conversion Error' Event
        /// </summary>
        public event ConversionErrorEventHandler ConversionError;

        /// <summary>
        /// The 'On Conversion Error' Event Handler: Called when an attempted conversion of a SCORM package fails.
        /// </summary>
        /// <param name="e">The 'Conversion Error' Event Arguments.</param>
        protected virtual void OnConversionError(ConversionErrorEventArgs e)
        {
            ConversionErrorEventHandler Handler = ConversionError;
            if (Handler != null)
            {
                foreach (ConversionErrorEventHandler Caster in Handler.GetInvocationList())
                {
                    ISynchronizeInvoke SyncInvoke = Caster.Target as ISynchronizeInvoke;
                    try
                    {
                        if (SyncInvoke != null && SyncInvoke.InvokeRequired)
                            SyncInvoke.Invoke(Handler, new object[] { this, e });
                        else
                            Caster(this, e);
                    }
                    catch
                    { }
                }
            }
        }

        /// <summary>
        /// The 'Conversion Log' Event
        /// </summary>
        public event ConversionLogEventHandler ConversionLog;

        /// <summary>
        /// The 'On Conversion Log' Event Handler: Called when the program writes to some output.
        /// </summary>
        /// <param name="e">The 'Conversion Log' Event Arguments.</param>
        protected virtual void OnConversionLog(ConversionLogEventArgs e)
        {
            ConversionLogEventHandler Handler = ConversionLog;
            if (Handler != null)
            {
                foreach (ConversionLogEventHandler Caster in Handler.GetInvocationList())
                {
                    ISynchronizeInvoke SyncInvoke = Caster.Target as ISynchronizeInvoke;
                    try
                    {
                        if (SyncInvoke != null && SyncInvoke.InvokeRequired)
                            SyncInvoke.Invoke(Handler, new object[] { this, e });
                        else
                            Caster(this, e);
                    }
                    catch
                    { }
                }
            }
        }


        #endregion

        #region Public Properties

        /// <summary>
        /// The input files.
        /// </summary>
        public FileInfo[] InputFiles
        {
            get { return this.inputFiles; }
            set
            {
                if (conversionStatus != Scorm2ccStatus.Stopped)
                {
                    throw new ApplicationException("Attempt to set property 'inputFiles' while conversion status was not 'Stopped'");
                }
                this.inputFiles = value;
            }
        }

        /// <summary>
        /// The output directory.
        /// </summary>
        public DirectoryInfo Output
        {
            get { return this.output; }
            set
            {
                if (conversionStatus != Scorm2ccStatus.Stopped)
                {
                    throw new ApplicationException("Attempt to set property 'Output' while conversion status was not 'Stopped'");
                }
                this.output = value;
            }
        }

        /// <summary>
        /// The SCORM 1.2 XSLT.
        /// </summary>
        public FileInfo XslFileForScorm1p2
        {
            get { return this.xslFileForScorm1p2; }
            set
            {
                if (conversionStatus != Scorm2ccStatus.Stopped)
                {
                    throw new ApplicationException("Attempt to set property 'XslFileForScorm1p2' while conversion status was not 'Stopped'");
                }
                this.xslFileForScorm1p2 = value;
            }
        }

        /// <summary>
        /// The SCORM 2004 XSLT.
        /// </summary>
        public FileInfo XslFileForScorm2004
        {
            get { return this.xslFileForScorm2004; }
            set
            {
                if (conversionStatus != Scorm2ccStatus.Stopped)
                {
                    throw new ApplicationException("Attempt to set property 'XslFileForScorm2004' while conversion status was not 'Stopped'");
                }
                this.xslFileForScorm2004 = value;
            }
        }


        #endregion

        #region Public Methods

        /// <summary>
        /// The 'Start Conversion' Method: Async Call to Start the Conversion Process.
        /// </summary>

        public void StartConversion(
        FileInfo[] inputFiles,
        DirectoryInfo output,
        FileInfo xslFileForScorm1p2,
        FileInfo xslFileForScorm2004)
        {
            try
            {
                this.inputFiles = inputFiles;
                this.output = output;
                this.xslFileForScorm1p2 = xslFileForScorm1p2;
                this.xslFileForScorm2004 = xslFileForScorm2004;

                StartConversion();
            }
            catch
            { throw; }
        }


        /// <summary>
        /// The 'StartConversion' Method: Async Call to Start the Conversion Process.
        /// </summary>
        public void StartConversion()
        {
            IOUtils ioUtils = new IOUtils();
            DirectoryInfo application = ioUtils.GetApplicationDirectory();

            try
            {
                if (conversionStatus != Scorm2ccStatus.Stopped)
                {
                    throw new ApplicationException("Attempt to call 'StartConversion' Failed - Status is not 'Stopped'");
                }

                cancelConversion = false;

                // Input validation
                if (this.inputFiles == null || this.inputFiles.Length == 0)
                {
                    throw new NullReferenceException(String.Format("Input files not defined: {0}", this.inputFiles));
                }

                foreach (FileInfo file in this.inputFiles){
                    if(!file.Exists){
                        throw new IOException(String.Format("Input file not found: {0}", file.FullName));
                    }
                }  

                // Output validation
                if (this.output == null)
                {
                    throw new NullReferenceException(String.Format("Output directory not defined: {0}", this.output));
                }
                if (!this.output.Exists)
                {
                    ioUtils.MakeDirectoriesInPath(this.output);
                }

                // SCORM 1.2 validation
                if (this.xslFileForScorm1p2 == null)
                {
                    throw new IOException(String.Format("SCORM 1.2 XSLT file not defined: {0}", this.xslFileForScorm1p2));
                }

                if (!this.xslFileForScorm1p2.Exists)
                {
                    throw new IOException(String.Format("SCORM 1.2 XSLT file not found: {0}", this.xslFileForScorm1p2.FullName));
                }

                // SCORM 2004 validation
                if (this.xslFileForScorm2004 == null)
                {
                    // The default
                    throw new IOException(String.Format("SCORM 2004 XSLT file not defined: {0}", this.xslFileForScorm2004));
                }

                if (!this.xslFileForScorm2004.Exists)
                {
                    throw new IOException(String.Format("SCORM 2004 XSLT file not found: {0}", this.xslFileForScorm2004.FullName));
                }

                // Queue the task.
                ThreadPool.QueueUserWorkItem(new WaitCallback(DoWork));
            }
            catch(Exception ex)
            {
                OnConversionError(new ConversionErrorEventArgs(ex));
            }
        }

        public void Update(int progressPackage, string statusPackage)
        {
            OnConversionProgress(new ConversionProgressEventArgs(
                this.overallProgress, 
                this.overallStatus, 
                progressPackage, 
                statusPackage));
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// The Main Work Method of the Converter Class: Handled in a Separate Thread.
        /// </summary>
        /// <param name="StateInfo">Undefined</param>
        private void DoWork(object StateInfo)
        {
            conversionStatus = Scorm2ccStatus.Started;
            OnConversionStart(new ConversionStartEventArgs());

            try
            {
                IObserver packageObserver = new PackageObserverImpl(this);

                // Utilities
                IOUtils ioUtils = new IOUtils();

                // Set up the paths and files
                //FileInfo[] fileInfos = input.GetFiles("*.zip");
                int count = this.inputFiles.Length;
                if (count == 0)
                {
                    throw new Scorm2CCException("Input folder does not contain any.zip files.");
                }

                // Extract SCORM files to a TEMP directory
                DirectoryInfo temp = new DirectoryInfo(System.Environment.ExpandEnvironmentVariables("%TEMP%"));
                DirectoryInfo application = ioUtils.GetApplicationDirectory();
                DirectoryInfo scormPackage = null;
                DirectoryInfo commonCartridge = null;
                DirectoryInfo scorm2ccResource = ioUtils.CombineParentAndChildDirectoryNames(application, "scorm2cc");

                // Get the implemntation class names
                string zipUtilsClassName = ConfigurationManager.AppSettings[PackageConverterFactory.ZIP_UTILS_IMPL_KEY];
                OnConversionLog(new ConversionLogEventArgs(Level.Info, String.Format("Using IZipUtils implementation class name: {0}", zipUtilsClassName), null));
                string packageConverterClassName =ConfigurationManager.AppSettings[PackageConverterFactory.PACKAGE_CONVERTER_IMPL_KEY];
                OnConversionLog(new ConversionLogEventArgs(Level.Info, String.Format("Using IPackageConverter implementation class name: {0}", packageConverterClassName), null));

                // Create the concrete classes
                PackageConverterFactory factory = PackageConverterFactory.Instance;
                IZipUtils zipUtils = null;
                IPackageConverter packageConverter = null;
                try
                {
                    zipUtils = factory.CreateZipUtils(zipUtilsClassName);
                    packageConverter = factory.CreatePackageConverter(packageConverterClassName);
                }
                catch (Exception e)
                {
                    OnConversionError(new ConversionErrorEventArgs(e));
                    conversionStatus = Scorm2ccStatus.Stopped;
                    return;
                }

                int i = 0;
                foreach (FileInfo file in this.inputFiles)
                {
                    i++;

                    this.overallProgress = Convert.ToInt32(100 * (Convert.ToDecimal(i) / Convert.ToDecimal(count)));
                    this.overallStatus = String.Format("Package: {0} ({1} bytes) {2} of {3}", file.Name, file.Length, i, count);

                    OnConversionLog(new ConversionLogEventArgs(Level.Info, String.Format("Scorm2CC conversion starting for SCORM package {0}, {1} bytes", 
                        file.Name, file.Length), null));

                    // Set up directories
                    scormPackage = ioUtils.CombineParentAndChildDirectoryNames(temp, ioUtils.GetFileNameWithoutExtension(file));
                    commonCartridge = ioUtils.CombineParentAndChildDirectoryNames(output, ioUtils.GetFileNameWithoutExtension(file));

                    // Unzip
                    zipUtils.UnZipFiles(
                        packageObserver, 
                        file,
                        scormPackage,
                        null,
                        false);

                    if (packageConverter.ContainsImsManifest(scormPackage, null))
                    {
                        OnConversionLog(new ConversionLogEventArgs(Level.Debug, String.Format("Copying resources for {0}", file.Name), null));
                        packageConverter.WrapAndCopyResourceFiles(scormPackage, commonCartridge, scorm2ccResource, packageObserver);

                        OnConversionLog(new ConversionLogEventArgs(Level.Debug, String.Format("Transforming {0}/imsmanifest", file.Name), null));
                        packageConverter.TransformImsManifest(scormPackage, commonCartridge, xslFileForScorm1p2, xslFileForScorm2004, packageObserver);
                    }
                    else
                    {
                        OnConversionLog(new ConversionLogEventArgs(Level.Debug, String.Format("Package {0} does not contain an imsmanifest.xml file, skipping.", file.Name), null));
                        DirectoryInfo toBeDeleted = ioUtils.CombineParentAndChildDirectoryNames(commonCartridge, file.Name);
                        try
                        {
                            //Directory.Delete(toBeDeleted.FullName, false);//gives error
                        }
                        catch (Exception e)
                        {
                            //Log(String.Format("Could not detete directory: {0}", toBeDeleted.FullName), e);
                        }

                        continue;
                    }

                    // Zip
                    OnConversionLog(new ConversionLogEventArgs(Level.Debug, String.Format("Zipping Common Cartridge {0}", file.Name), null));
                    zipUtils.ZipFiles(
                        packageObserver, 
                        commonCartridge,
                        new FileInfo(commonCartridge.FullName + ".zip"),
                        null,
                        1);

                    // Clean up
                    Directory.Delete(scormPackage.FullName, true);
                    //Directory.Delete(commonCartridge.FullName, true); // gives error


                    OnConversionLog(new ConversionLogEventArgs(Level.Info, String.Format("Scorm2CC conversion completed for SCORM package {0}", file.Name), null));
                }

            }
            catch (Exception e)
            {
                OnConversionError(new ConversionErrorEventArgs(e));
            }
            finally
            {
                conversionStatus = Scorm2ccStatus.Stopped;
                OnConversionComplete(new ConversionCompleteEventArgs(cancelConversion));
            }
        }

        #endregion
    }
}
